How to make unit tests with several localizations

Since Xcode 11 there is a possibility to configure different locales for unit tests by using Test Plans. It is fine, but a little bit cumbersome as requires the Xcode-specific configuration. Another way of making it work is the old-school method swizzling. It is quite simple to do and does its job with quite nice flexibility. It doesn't require any Xcode setup, works well with SPM too. The idea could be also extended to customize not only the Locale, but also e.g. preferredlanguages. The key point is that we can do it by exchanging the methods on NSLocale instead of Locale which still is used as a wrapper over the objective-c predecessor.

We can use method_setImplementation and @convention(block) (swift documentation) in this case, which makes it a little bit more ergonomic than defining the @objc method that would solve the purpose of the exchanged method.

extension XCTestCase {
   func setLocale(identifier: String, preferredLanguages: [String]) {
        let currentlLocale: @convention(block) (AnyObject)
            -> AnyObject = { (_: AnyObject!) -> NSLocale in
                return NSLocale(localeIdentifier: identifier)
            }

        method_setImplementation(
            class_getClassMethod(NSLocale.self, #selector(getter: NSLocale.current))!,
            imp_implementationWithBlock(currentlLocale)
        )

        let preferredLanguages: @convention(block) (AnyObject)
            -> [String] = { (_: AnyObject!) -> [String] in
                return preferredLanguages
            }

        method_setImplementation(
            class_getClassMethod(NSLocale.self, #selector(getter: NSLocale.preferredLanguages))!,
            imp_implementationWithBlock(preferredLanguages)
        )
    }
}

Having that we can easily change the Locale for each test. The only downside is that you still use singleton Locale.current, so running tests in parallel will not work reliably.

class Test: XCTestCase {
    func test_locale() {
        setLocale(identifier: "fr", preferredLanguages: ["fr", "de", "pl"])

        XCTAssertEqual(Locale.current, .init(identifier: "fr"))
        XCTAssertEqual(Locale.preferredLanguages, ["fr", "de", "pl"])
    }
}
Tagged with: